// @ts-ignore
import { buildExternalHelpers, loadPartialConfigAsync, TransformOptions } from '@babel/core'
import { createComplugin, SourceMap, utils } from 'complugin'
import { createFastWorker } from 'fast-worker'
import { isString, once } from 'lodash-es'
import LRUCache from 'lru-cache'
import path from 'path'
import { fileURLToPath } from 'url'
import { HELPERS } from './helpers'
import * as _transformModule from './transform'
import { babelConfigItemToPluginItem, babelOptionsToJson } from './utils'
// @ts-ignore
import structuredClone from 'core-js-pure/actual/structured-clone'

type BabelOptions = Omit<TransformOptions, 'fileName' | 'minify' | 'include' | 'exclude'>

const initTransformCache = once(() => {
  let babelTransformCache: LRUCache<string, [optionsAndCode: string, map: SourceMap | undefined]> = (globalThis as any)
    .__babelTransformLRUCache

  if (!(babelTransformCache instanceof LRUCache)) {
    ;(globalThis as any).__babelTransformLRUCache = babelTransformCache = new LRUCache({
      max: 0x100,
      maxSize: 0x1ffffff,
      sizeCalculation: (value, key) => (value[1] ? 3 : 1) * key.length
    })
  }

  return babelTransformCache
})

declare namespace compluginBabel {
  interface Options extends Omit<BabelOptions, 'wrapPluginVisitorMethod'> {
    /**
     * A minimatch pattern, or array of patterns,
     * which specifies the files in the build the plugin should ignore.
     *
     * When relying on Babel configuration files you can only exclude additional files with this option,
     * you cannot override what you have configured for Babel itself.
     *
     * @default [/\.(tsx?|jsx?|mjs)$/i]
     */
    include?: (string | RegExp)[]
    /**
     * A minimatch pattern, or array of patterns, which specifies the files in the build the plugin should operate on.
     * When relying on Babel configuration files you cannot include files already excluded there.
     */
    exclude?: (string | RegExp)[]
    /**
     * Cache the transform results of Babel.
     *
     * @default true
     */
    cache?: boolean
    /**
     * Use worker threads for parallel transformations.
     * Enabling parallel mode is only recommended for large projects.
     * @default false
     */
    parallel?: boolean
  }
}

const JS_TRANSFORM_CACHED_LIMIT = 512
// @ts-ignore
const _fileName = typeof __filename === 'string' ? __filename : fileURLToPath(import.meta.url)
const TRANSFORM_JS = path.join(path.dirname(_fileName), 'transform.js')
const BABEL_HELPERS_JS = path.join(path.dirname(_fileName), 'helpers.js')

const compluginBabel = createComplugin<compluginBabel.Options>({
  name: 'babel',
  enforce: undefined,
  factory(options, meta) {
    let CWD = process.cwd()
    if (meta.framework === 'esbuild') {
      CWD = meta.build.initialOptions.absWorkingDir ?? CWD
    }

    const {
      // @ts-ignore
      include: _include = [/\.(tsx?|jsx?|mjs)$/i],
      exclude: _exclude,
      cwd,
      cache = true,
      parallel = false,
      ..._babelOptions
    } = options ?? {}
    const babelOptions: import('@babel/core').TransformOptions = {
      ..._babelOptions,
      cloneInputAst: false,
      ast: false,
      code: true,
      sourceMaps: true,
      cwd: cwd ?? CWD
    }

    const filter = utils.createFilter(_include, _exclude)
    const transformCache = cache ? initTransformCache() : undefined

    const getTransformOptionsAndWatchFiles = once(async () => {
      ;(babelOptions.plugins = babelOptions.plugins ?? []).concat([BABEL_HELPERS_JS])
      const config = await loadPartialConfigAsync(babelOptions)
      const watchFiles = new Set<string>()

      let options = config?.options ?? {}

      const presets = (options.presets = options.presets ?? [])
      const plugins = (options.plugins = options.plugins ?? [])

      delete options.wrapPluginVisitorMethod
      delete options.babelrc
      delete options.babelrcRoots
      delete options.configFile
      delete options.browserslistConfigFile

      let allowParallel = parallel

      if (allowParallel) {
        options.presets = presets.map(babelConfigItemToPluginItem)
        options.plugins = plugins.map(babelConfigItemToPluginItem)

        try {
          // @ts-ignore
          options = structuredClone(options)
        } catch (error) {
          options.presets = presets
          options.plugins = plugins

          allowParallel = false
        }
      }

      if (config) {
        // Availabe since Babel 7.12
        // https://github.com/babel/babel/pull/11907
        if ((config as any).files) {
          ;(config as any).files.forEach?.((configFile: string) => watchFiles.add(configFile))
        } else {
          // .babelrc.json
          if (typeof config.babelrc === 'string') {
            watchFiles.add(config.babelrc)
          }
          // babel.config.js
          if (config.config) {
            watchFiles.add(config.config)
          }
        }
      }

      return [options, babelOptionsToJson(options), watchFiles, allowParallel] as const
    })

    if (meta.framework === 'esbuild') {
      meta.onResolveRegExp = new RegExp(`^${utils.escapeRegExp(HELPERS)}$`)
    }

    let transformModule = _transformModule
    let transformCode = transformModule.transform

    return {
      async buildStart() {
        const [_, _1, _2, allowParallel] = await getTransformOptionsAndWatchFiles()

        if (allowParallel) {
          transformModule = createFastWorker(TRANSFORM_JS) as any
          transformCode = transformModule.transform
        } else {
          this.warn(`The current Babel configuration does not support parallelism.`)
        }
      },
      buildEnd() {
        transformModule.dispose()
        transformModule = _transformModule
        transformCode = transformModule.transform
      },
      resolveId: source => (source === HELPERS ? source : undefined),
      load: id => {
        if (id === HELPERS) {
          return buildExternalHelpers(null, 'module')
        }
      },
      transformInclude: id => (id === HELPERS ? false : filter(id)),
      async transform(originalCode, id, ast) {
        const [transformOptions, transformOptionsStr, watchFiles] = await getTransformOptionsAndWatchFiles()

        const optionsAndCode =
          !!transformCache && originalCode.length >= JS_TRANSFORM_CACHED_LIMIT && transformOptionsStr + originalCode

        const finallyOptions: TransformOptions = {
          ...transformOptions,
          filename: id
        }

        if (optionsAndCode) {
          const result = transformCache.get(optionsAndCode)
          if (result) {
            const [code, map] = result
            return {
              code,
              map: this.sourceMap ? map : undefined
            }
          }
        } else {
          finallyOptions.sourceMaps = this.sourceMap
          for (const watchFile of watchFiles) {
            this.addWatchFile(watchFile)
          }
        }

        let result
        try {
          result = await transformCode(originalCode, finallyOptions)
        } catch {}

        if (result) {
          let { code, map } = result

          if (code != null) {
            if (optionsAndCode) {
              transformCache.set(optionsAndCode, [code, isString(map) ? JSON.parse(map) : map])
            }

            return { code, map: this.sourceMap ? map : undefined }
          }
        }
      }
    }
  }
})

export default compluginBabel
